package com.googlecode.cswish.struts.hibernate;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.log4j.Logger;
import org.hibernate.cache.EntityRegion;
import org.hibernate.cache.access.AccessType;
import org.hibernate.cache.access.EntityRegionAccessStrategy;
import org.hibernate.cache.impl.CacheDataDescriptionImpl;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.cfg.Settings;
import org.hibernate.connection.ConnectionProvider;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.Mapping;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.impl.SessionFactoryImpl;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.RootClass;
import org.hibernate.mapping.SimpleValue;
import org.hibernate.mapping.Table;
import org.hibernate.persister.PersisterFactory;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.tool.hbm2ddl.DatabaseMetadata;
import org.hibernate.tool.hbm2ddl.TableMetadata;
import org.hibernate.util.StringHelper;
import org.w3c.dom.Document;

import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.struts.AutoValidatorManager;
import com.googlecode.cswish.struts.conversion.AutoPageConverterManager;
import com.googlecode.cswish.struts.spring.BeanIntrospector;


/**
 * Maintain Hibernate model meta data - Add/Remove model field
 * 	 
 * @author jerry.feng Date: 2009-2-9
 */
public class DynamicModelDef {

	private static final Logger logger = Logger.getLogger(DynamicModelDef.class);
	
	// The model is mixed model
	private Component dynamicProperties;
	
	// Class<? extends IDynamicModel or Map or IMixedModel>
    private String entityName;
    private BeanIntrospector beanIntrospector;
    private AutoPageConverterManager converterManager;
    private AutoValidatorManager autoValidatorManager;
    
    private HibernateConfiguration hibernateConfiguration;
    
    boolean changed;
	
    public DynamicModelDef(String entityName, BeanIntrospector beanIntrospector,
    		AutoPageConverterManager converterManager, AutoValidatorManager autoValidatorManager,
    		HibernateConfiguration hibernateConfiguration) {
        this.entityName = entityName;
        this.beanIntrospector = beanIntrospector;
        this.hibernateConfiguration = hibernateConfiguration;
        this.converterManager = converterManager;
        this.autoValidatorManager = autoValidatorManager;
        
        Iterator<Property> propertyIterator = getPersistentClass(entityName).getPropertyIterator();
        while (propertyIterator.hasNext()) {
            Property property = propertyIterator.next();
            if (property.getName().equals(IMixedModel.DYNAMIC_PROP_NAME)) {
            	dynamicProperties = (Component) property.getValue();
            	break;
            }
        }
    }
	
    public String getEntityName() {
		return entityName;
	}
    
    /**
     * Update the dynamic model information according to the modelInfo
     * 
     * @param modelInfo
     */
    public void updateModelInfo(ModelInfo modelInfo) {
    	for (PropertyInfo propertyInfo : modelInfo.getPersistenceProperties()) {
    		if (propertyInfo.isTableColumn()) {
	    		addDynamicField(propertyInfo.getName(), propertyInfo.getColumnName(),
	    				propertyInfo.getType(), propertyInfo.getLength(),
	    				propertyInfo.isNullable(), propertyInfo.isUnique(),
	    				propertyInfo.isInsertable(), propertyInfo.isUpdatable());
    		}
    	}
    	commitFieldUpdate();
    }
    
    public void addDynamicField(String name, String type) {
    	addDynamicField(name, name, type, 0, false, false, true, true);
    }
    
    public void addDynamicField(String name, String columnName,
    		String type, int length, boolean isNullable, boolean unique,
    		boolean isInsertable, boolean isUpdatable) {
    	if (name == null) {
    		logger.error("can't add null property to hibernate model:" + entityName);
    		return;
    	}
    	
    	changed = true;
		PersistentClass persistentClass = getPersistentClass(entityName);
		
		// see HbmBinder.bindColumn
        Column column = new Column(name);
        persistentClass.getTable().addColumn(column);
        
        if (length != 0) {
        	column.setLength(length);
        }
        column.setNullable(isNullable);
        column.setUnique(unique);
        
		SimpleValue simpleValue = new SimpleValue();
        simpleValue.addColumn(column);
        simpleValue.setTypeName(type);        
        simpleValue.setTable(persistentClass.getTable());

        Property property = getProperty(persistentClass, name);
        if (property == null && dynamicProperties != null) {
        	property = dynamicProperties.getProperty(name);
        }
        
        boolean isAdd;
        // Add new property
        if (property == null) {
        	isAdd = true;
        	property = new Property();
        } else {
        	isAdd = false;
        }
        property.setName(name);
        property.setNodeName(columnName);
        property.setValue(simpleValue);
        property.setUpdateable(isUpdatable);
        property.setInsertable(isInsertable);
        
        if (isAdd) {
	        if (dynamicProperties == null) {
	        	persistentClass.addProperty(property);
	        } else {
	        	dynamicProperties.addProperty(property);
	        }
        }
    }
    
    private Property getProperty(PersistentClass persistentClass, String name) {
    	Iterator iterator = persistentClass.getPropertyClosureIterator();
		Property identifierProperty = persistentClass.getIdentifierProperty();
		String propertyName = StringHelper.root(name);
		if ( identifierProperty != null
				&& propertyName.equals(identifierProperty.getName())
				) {
			return identifierProperty;
		}
		else {
			while (iterator.hasNext()) {
				Property prop = (Property) iterator.next();
				if ( propertyName.equals(prop.getName())) {
					return prop;
				}
			}
		}
		return null;
    }

    public void removeDynamicField(String name) {
    	changed = true;
    	
    	Iterator<Property> propertyIterator;
    	if (dynamicProperties == null) {
    		propertyIterator = getPersistentClass(entityName).getPropertyIterator();
    	} else {
	        propertyIterator = dynamicProperties.getPropertyIterator();
    	}
        while (propertyIterator.hasNext()) {
            Property property = propertyIterator.next();
            if (property.getName().equals(name)) {
                propertyIterator.remove();
                return;
            }
        }
    }
    
    public void dropSelf() {
    	beanIntrospector.reset(entityName);
    	converterManager.resetAll(entityName);
    	autoValidatorManager.resetAll(entityName);
    	
    	updateDBSchema(entityName, true);
    	List<Configuration> configs = hibernateConfiguration.getConfigurations();
    	for (Configuration config : configs) {
    		PersistentClass model = config.getClassMapping(entityName);
    		if (model != null) {
    			Map classes = (Map) getField(config, Configuration.class, "classes");
    			if (classes != null) {
    				classes.remove(entityName);
    			}
    			removeModelFromFactory(model);
    			break;
    		}
    	}
    }

    public synchronized void commitFieldUpdate() {
    	if (changed) {
	        beanIntrospector.reset(entityName);
	        converterManager.resetAll(entityName);
	    	autoValidatorManager.resetAll(entityName);
	    	
	        updateSessionFactory(entityName);
	        updateDBSchema(entityName, false);
	        
	        changed = false;
    	}
    }
    
    //@see Configuration.generateSchemaUpdateScript & SchemaUpdate.execute
    private boolean updateDBSchema(String entityName, boolean isDrop) {
    	Connection connection = null;
		Statement stmt = null;
    	
		try {
			SessionFactoryImpl sessionFactory = (SessionFactoryImpl)hibernateConfiguration.getSessionFactory();
			Settings settings = sessionFactory.getSettings();
			List<Configuration> configs = hibernateConfiguration.getConfigurations();
			Configuration config = configs.iterator().next();
			Mapping mapping = config.buildMapping();
			Properties properties = config.getProperties();
			
			
			Dialect dialect = settings.getDialect();
			ConnectionProvider connectionProvider = settings.getConnectionProvider();
			connection = connectionProvider.getConnection();
			Table table = getPersistentClass(entityName).getTable();
			String defaultCatalog = properties.getProperty( Environment.DEFAULT_CATALOG );
			String defaultSchema = properties.getProperty( Environment.DEFAULT_SCHEMA );
			DatabaseMetadata databaseMetadata = new DatabaseMetadata( connection, dialect );
			TableMetadata tableInfo = databaseMetadata.getTableMetadata(
					table.getName(),
					( table.getSchema() == null ) ? defaultSchema : table.getSchema(),
					( table.getCatalog() == null ) ? defaultCatalog : table.getCatalog(),
							table.isQuoted()
			);
			
			if (isDrop) {
				if (tableInfo != null) {
					String sql = table.sqlDropString(dialect, defaultCatalog, defaultSchema);
					stmt = connection.createStatement();
					stmt.executeUpdate(sql);
					logger.debug(sql);
				}
			} else {
				if (tableInfo == null) {
					// create a new table
					String sql = table.sqlCreateString(dialect, mapping, defaultCatalog, defaultSchema);
					stmt = connection.createStatement();
					stmt.executeUpdate(sql);
					logger.debug(sql);
				} else {
					Iterator<String> iter = table.sqlAlterStrings(dialect, mapping, tableInfo, defaultCatalog, defaultSchema);
					
					while (iter.hasNext()) {
						String sql = iter.next();
						stmt = connection.createStatement();
						stmt.executeUpdate(sql);
						logger.debug(sql);
					}
				}
			}
		} catch (Exception ex) {
			logger.error("Update DB Schema", ex);
			return false;
		} finally {
			try {
				if ( stmt != null ) {
					stmt.close();
				}
				if (connection != null) {
					connection.close();
				}
			}
			catch ( Exception e ) {
				logger.error( "Error closing connection", e );
			}
		}
		return true;
	}
    
    //@see http://www.javaeye.com/topic/197752
    //@see SessionFactoryImpl()
    @SuppressWarnings("unchecked")
	private void updateSessionFactory(String entityName) {
    	PersistentClass model = getPersistentClass(entityName);
    	List<Configuration> configs = hibernateConfiguration.getConfigurations();
		Configuration config = configs.iterator().next();
		SessionFactoryImpl sessionFactory = (SessionFactoryImpl)hibernateConfiguration.getSessionFactory();
		Settings settings = sessionFactory.getSettings();
		
    	Mapping mapping = config.buildMapping();
		if (!model.isInherited()) {
			IdentifierGenerator generator = model.getIdentifier()
					.createIdentifierGenerator(settings.getDialect(),
							settings.getDefaultCatalogName(),
							settings.getDefaultSchemaName(), (RootClass) model);
			Map identifierGenerators = (Map) getField(sessionFactory, "identifierGenerators");
			if (!identifierGenerators.containsKey(model.getEntityName()))
				identifierGenerators.put(model.getEntityName(), generator);
		}

		model.prepareTemporaryTables(mapping, settings.getDialect());

		Properties properties = (Properties) getField(sessionFactory, "properties");
		final String cacheRegionPrefix = settings.getCacheRegionPrefix() == null ? "" : settings.getCacheRegionPrefix() + ".";
		final String cacheRegionName = cacheRegionPrefix + model.getRootClass().getCacheRegionName();
		// cache region is defined by the root-class in the hierarchy...
		EntityRegionAccessStrategy accessStrategy = null;
		if (settings.isSecondLevelCacheEnabled() ) {
			final AccessType accessType = AccessType.parse( model.getCacheConcurrencyStrategy() );			
			if ( accessType != null ) {
				EntityRegion entityRegion = settings.getRegionFactory().buildEntityRegion( cacheRegionName, properties, CacheDataDescriptionImpl.decode( model ) );
				accessStrategy = entityRegion.buildAccessStrategy( accessType );
				Map allCacheRegions = (Map) getField(sessionFactory, "allCacheRegions");
				allCacheRegions.put(cacheRegionName, entityRegion);
			}
		}
		EntityPersister cp = PersisterFactory.createClassPersister(model,
				accessStrategy, sessionFactory, mapping);
		
		Map entityPersisters = (Map) getField(sessionFactory, "entityPersisters");
		entityPersisters.put(model.getEntityName(), cp);
		
		Map imports = (Map) getField(sessionFactory, "imports");
		String simpleName = entityName.substring(entityName.lastIndexOf('.') + 1);
		imports.put(simpleName, entityName);
	}
    
    @SuppressWarnings("unchecked")
    private void removeModelFromFactory(PersistentClass model) {
    	String entityName = model.getEntityName();
    	SessionFactoryImpl sessionFactory = (SessionFactoryImpl)hibernateConfiguration.getSessionFactory();
    	Map identifierGenerators = (Map) getField(sessionFactory, "identifierGenerators");
    	if (identifierGenerators != null) {
    		identifierGenerators.remove(entityName);
    	}
    	String cacheRegion = model.getRootClass().getCacheRegionName();
    	Map allCacheRegions = (Map) getField(sessionFactory, "allCacheRegions");
    	if (allCacheRegions != null) {
    		allCacheRegions.remove(cacheRegion);
    	}
    	Map entityPersisters = (Map) getField(sessionFactory, "entityPersisters");
    	if (entityPersisters != null) {
    		entityPersisters.remove(entityName);
    	}
    }
    
    private Object getField(Object object, String fieldName) {
    	return getField(object, object.getClass(), fieldName);
    }
    
    private Object getField(Object object, Class clazz, String fieldName) {
    	Object value = null;
    	try {
	    	Field field = clazz.getDeclaredField(fieldName);
	    	field.setAccessible(true);
	    	value = field.get(object);
    	} catch (Exception ex) {
    		logger.error("Fail to get field: " + fieldName);
    	}
    	return value;
    }

    private PersistentClass getPersistentClass(String entityName) {
    	PersistentClass clazz = hibernateConfiguration.getClassMapping(entityName);
    	
    	if (clazz == null) {
    		// define a new mapping according to the template
    		List<Configuration> configs = hibernateConfiguration.getConfigurations();
    		Configuration config = configs.iterator().next();
    		Document document = null;
    		try {
	    		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	            DocumentBuilder builder = factory.newDocumentBuilder();
	            document = builder.parse(this.getClass().getResourceAsStream("DynamicModelTemplate.xml"));
    		} catch (Exception ex) {
    			logger.error("Fail to load DynamicModelTemplate.xml for entity:" + entityName, ex);
    		}
    		int index = entityName.lastIndexOf('.');
    		String tableName = entityName.substring(index + 1);
    		MappingManager.updateClass(document, entityName, tableName);
    		config.addDocument(document);
    		config.buildMappings();
    		clazz = config.getClassMapping(entityName);
    	}
    	return clazz;
    }
}